Skip to content

Conversation

@BenHenning
Copy link
Collaborator

@BenHenning BenHenning commented Apr 22, 2025

The basics

The details

Resolves

Fixes #8918
Fixes #8919
Fixes part of #8771

Proposed Changes

This updates several classes in order to make toolboxes and flyouts focusable:

  • IFlyout is now an IFocusableTree with corresponding implementations in FlyoutBase.
  • IToolbox is now an IFocusableTree with corresponding implementations in Toolbox.
  • IToolboxItem is now an IFocusableNode with corresponding implementations in ToolboxItem.
  • As the primary toolbox items, ToolboxCategory and ToolboxSeparator were updated to have -1 tab indexes and defined IDs to help ToolboxItem fulfill its contracted for IFocusableNode.getFocusableElement.

Each of these two new focusable trees have specific noteworthy behaviors behaviors:

  • Toolbox will automatically indicate that its first item should be focused (if one is present), even overriding the ability to focus the toolbox's root (however there are some cases where that can still happen).
  • Toolbox will automatically synchronize its selection state with its item nodes being focused.
  • FlyoutBase, now being a focusable tree, has had a tab index of 0 added. Normally a tab index of -1 is all that's needed, but the keyboard navigation plugin specifically uses 0 for flyout so that the flyout is tabbable. This is a new tab stop being introduced.
  • FlyoutBase holds a workspace (for rendering blocks) and, since WorkspaceSvg is already set up to be a focusable tree, it's represented as a subtree to FlyoutBase. This does introduce some wonky behaviors: the flyout's root will have passive focus while its contents have active focus. This could be manually disabled with some CSS if it ends up being a confusing user experience.
  • Both FlyoutBase and WorkspaceSvg have built-in behaviors for detecting when a user tries navigating away from an open flyout to ensure that the flyout is closed when it's supposed to be. That is, the flyout is auto-hideable and a non-flyout, non-toolbox node has then been focused. This matches parity with the T/Esc flows supported in the keyboard navigation plugin playground.

One other thing to note: Toolbox had a few tests to update that were trying to reinit a toolbox without first disposing of it (which was caught by one of FocusManager's state guardrails).

Reason for Changes

This is part of an ongoing effort to ensure key components of Blockly are focusable so that they can be keyboard-navigable (with other needed changes yet both in Core Blockly and the keyboard navigation plugin).

Test Coverage

No new tests have been added. It's certainly possible to add unit tests for the focusable configurations being introduced in this PR, but it may not be highly beneficial. It's largely assumed that the individual implementations should work due to a highly tested FocusManager, and it may be the case that the interactions of the components working together is far more important to verify (that is, the end user flows). The latter is planned to be tackled as part of #8915.

Documentation

No documentation changes should be needed here.

Additional Information

This includes changes that have been pulled from #8875.

This adds basic support for WorkspaceSvg being focusable via its blocks,
though fields and connections are not yet supported.
This introduces the fundamental support needed to ensure that both
toolboxes and flyouts are focusable using FocusManager.
Copy link
Collaborator Author

@BenHenning BenHenning left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-reviewed changes not part of #8916 (though note that this includes WorkspaceSvg changes on top of that PR's changes).

@BenHenning BenHenning marked this pull request as ready for review April 22, 2025 01:07
@BenHenning BenHenning requested a review from a team as a code owner April 22, 2025 01:07
@BenHenning BenHenning requested a review from maribethb April 22, 2025 01:07
@BenHenning
Copy link
Collaborator Author

BenHenning commented Apr 22, 2025

PTAL @maribethb. Note that files changed in #8916 and #8909 can be ignored except for WorkspaceSvg (since both this and #8916 make changes to that file).

@rachel-fenichel
Copy link
Collaborator

@BenHenning would you like me to take over reviewing this one since Maribeth is out of office?

@BenHenning
Copy link
Collaborator Author

Yes please @rachel-fenichel but I will bring this up-to-date momentarily after #8916 has had a couple of updates, and will assign it over to you.

@BenHenning BenHenning requested review from rachel-fenichel and removed request for maribethb April 23, 2025 22:28
@BenHenning
Copy link
Collaborator Author

PTAL @rachel-fenichel.

// root of the toolbox.
if (!previousNode || previousNode === this) {
return this.getToolboxItems().find((item) => item.isSelectable()) ?? null;
} else return null;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for the else, just do a newline and return null

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if (flyout && nextTree === flyout) return;
if (toolbox && nextTree === toolbox) return;
if (toolbox) toolbox.clearSelection();
if (flyout && flyout instanceof Flyout) flyout.autoHide(false);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What else would flyout be besides an instanceof Flyout?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's IFlyout in this context per: https://github.com/google/blockly/blob/ac7fea11cffc4c445cedae77675290861ceec2f6/core/workspace_svg.ts#L1014

I think logically it can only ever be Flyout but TypeScript doesn't know that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Makes sense.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's IFlyout in this context per:

https://github.com/google/blockly/blob/ac7fea11cffc4c445cedae77675290861ceec2f6/core/workspace_svg.ts#L1014

I think logically it can only ever be Flyout but TypeScript doesn't know that.

Using our base Flyout class is technically not required, only using the IFlyout interface, and I’m pretty sure one of the partners doesn’t use our base class. Instead you should use the IAutoHideable interface we have. Which our base class implements but isn’t required

Addresses reviewer comment.
Copy link
Collaborator

@rachel-fenichel rachel-fenichel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One nit then LGTM!

if (this.getSelectedItem() !== node) {
this.setSelectedItem(node as IToolboxItem);
}
} else this.clearSelection();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: use braces around the else

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

if (flyout && nextTree === flyout) return;
if (toolbox && nextTree === toolbox) return;
if (toolbox) toolbox.clearSelection();
if (flyout && flyout instanceof Flyout) flyout.autoHide(false);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Makes sense.

Copy link
Collaborator Author

@BenHenning BenHenning left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-reviewed changes after merging #8916. I don't think another review round is necessary as the changes look clean.

@BenHenning BenHenning changed the title feat: Make toolbox and flyout focusable [Blocked on: #8916] feat: Make toolbox and flyout focusable Apr 24, 2025
Not really necessary, but I like seeing green CI before merging.
Copy link
Collaborator Author

@BenHenning BenHenning left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty commit review.

@BenHenning
Copy link
Collaborator Author

Thanks for the reviews! Going ahead and merging this to keep the downstream PRs simpler.

@BenHenning BenHenning merged commit 5bc8380 into RaspberryPiFoundation:rc/v12.0.0 Apr 24, 2025
7 checks passed
@BenHenning BenHenning deleted the make-toolbox-and-flyout-focusable branch April 24, 2025 22:09
This was referenced Apr 24, 2025
RoboErikG added a commit that referenced this pull request Apr 25, 2025
RoboErikG added a commit to RoboErikG/blockly that referenced this pull request Apr 25, 2025
RoboErikG added a commit that referenced this pull request Apr 25, 2025
* Revert "feat: Make toolbox and flyout focusable (#8920)"

This reverts commit 5bc8380.

* Revert "feat: Make WorkspaceSvg and BlockSvg focusable (#8916)"

This reverts commit d7680cf.
@BenHenning BenHenning mentioned this pull request Apr 28, 2025
1 task
BenHenning added a commit to BenHenning/blockly that referenced this pull request Apr 29, 2025
## The basics

- [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change)

## The details
### Resolves

Fixes RaspberryPiFoundation#8918
Fixes RaspberryPiFoundation#8919
Fixes part of RaspberryPiFoundation#8771

### Proposed Changes

This updates several classes in order to make toolboxes and flyouts focusable:
- `IFlyout` is now an `IFocusableTree` with corresponding implementations in `FlyoutBase`.
- `IToolbox` is now an `IFocusableTree` with corresponding implementations in `Toolbox`.
- `IToolboxItem` is now an `IFocusableNode` with corresponding implementations in `ToolboxItem`.
- As the primary toolbox items, `ToolboxCategory` and `ToolboxSeparator` were updated to have -1 tab indexes and defined IDs to help `ToolboxItem` fulfill its contracted for `IFocusableNode.getFocusableElement`.

Each of these two new focusable trees have specific noteworthy behaviors behaviors:
- `Toolbox` will automatically indicate that its first item should be focused (if one is present), even overriding the ability to focus the toolbox's root (however there are some cases where that can still happen).
- `Toolbox` will automatically synchronize its selection state with its item nodes being focused.
- `FlyoutBase`, now being a focusable tree, has had a tab index of 0 added. Normally a tab index of -1 is all that's needed, but the keyboard navigation plugin specifically uses 0 for flyout so that the flyout is tabbable. This is a **new** tab stop being introduced.
- `FlyoutBase` holds a workspace (for rendering blocks) and, since `WorkspaceSvg` is already set up to be a focusable tree, it's represented as a subtree to `FlyoutBase`. This does introduce some wonky behaviors: the flyout's root will have passive focus while its contents have active focus. This could be manually disabled with some CSS if it ends up being a confusing user experience.
- Both `FlyoutBase` and `WorkspaceSvg` have built-in behaviors for detecting when a user tries navigating away from an open flyout to ensure that the flyout is closed when it's supposed to be. That is, the flyout is auto-hideable and a non-flyout, non-toolbox node has then been focused. This matches parity with the `T`/`Esc` flows supported in the keyboard navigation plugin playground.

One other thing to note: `Toolbox` had a few tests to update that were trying to reinit a toolbox without first disposing of it (which was caught by one of `FocusManager`'s state guardrails).

### Reason for Changes

This is part of an ongoing effort to ensure key components of Blockly are focusable so that they can be keyboard-navigable (with other needed changes yet both in Core Blockly and the keyboard navigation plugin).

### Test Coverage

No new tests have been added. It's certainly possible to add unit tests for the focusable configurations being introduced in this PR, but it may not be highly beneficial. It's largely assumed that the individual implementations should work due to a highly tested FocusManager, and it may be the case that the interactions of the components working together is far more important to verify (that is, the end user flows). The latter is planned to be tackled as part of RaspberryPiFoundation#8915.

### Documentation

No documentation changes should be needed here.

### Additional Information

This includes changes that have been pulled from RaspberryPiFoundation#8875.
@BenHenning BenHenning restored the make-toolbox-and-flyout-focusable branch April 30, 2025 01:28
BenHenning added a commit that referenced this pull request Apr 30, 2025
_Note: This is a roll forward of #8920 that was reverted in #8933. See Additional Information below._

## The basics

- [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change)

## The details
### Resolves

Fixes #8918
Fixes #8919
Fixes part of #8943
Fixes part of #8771

### Proposed Changes

This updates several classes in order to make toolboxes and flyouts focusable:
- `IFlyout` is now an `IFocusableTree` with corresponding implementations in `FlyoutBase`.
- `IToolbox` is now an `IFocusableTree` with corresponding implementations in `Toolbox`.
- `IToolboxItem` is now an `IFocusableNode` with corresponding implementations in `ToolboxItem`.
- As the primary toolbox items, `ToolboxCategory` and `ToolboxSeparator` were updated to have -1 tab indexes and defined IDs to help `ToolboxItem` fulfill its contracted for `IFocusableNode.getFocusableElement`.
- `FlyoutButton` is now an `IFocusableNode` (with corresponding ID generation, tab index setting, and ID matching for retrieval in `WorkspaceSvg`).

Each of these two new focusable trees have specific noteworthy behaviors behaviors:
- `Toolbox` will automatically indicate that its first item should be focused (if one is present), even overriding the ability to focus the toolbox's root (however there are some cases where that can still happen).
- `Toolbox` will automatically synchronize its selection state with its item nodes being focused.
- `FlyoutBase`, now being a focusable tree, has had a tab index of 0 added. Normally a tab index of -1 is all that's needed, but the keyboard navigation plugin specifically uses 0 for flyout so that the flyout is tabbable. This is a **new** tab stop being introduced.
- `FlyoutBase` holds a workspace (for rendering blocks) and, since `WorkspaceSvg` is already set up to be a focusable tree, it's represented as a subtree to `FlyoutBase`. This does introduce some wonky behaviors: the flyout's root will have passive focus while its contents have active focus. This could be manually disabled with some CSS if it ends up being a confusing user experience.
- Both `FlyoutBase` and `WorkspaceSvg` have built-in behaviors for detecting when a user tries navigating away from an open flyout to ensure that the flyout is closed when it's supposed to be. That is, the flyout is auto-hideable and a non-flyout, non-toolbox node has then been focused. This matches parity with the `T`/`Esc` flows supported in the keyboard navigation plugin playground.

One other thing to note: `Toolbox` had a few tests to update that were trying to reinit a toolbox without first disposing of it (which was caught by one of `FocusManager`'s state guardrails).

This only addresses part of #8943: it adds support for `FlyoutButton` which covers both buttons and labels. However, a longer-term solution may be to change `FlyoutItem` itself to force using an `IFocusableNode` as its element.

### Reason for Changes

This is part of an ongoing effort to ensure key components of Blockly are focusable so that they can be keyboard-navigable (with other needed changes yet both in Core Blockly and the keyboard navigation plugin).

### Test Coverage

No new tests have been added. It's certainly possible to add unit tests for the focusable configurations being introduced in this PR, but it may not be highly beneficial. It's largely assumed that the individual implementations should work due to a highly tested FocusManager, and it may be the case that the interactions of the components working together is far more important to verify (that is, the end user flows). The latter is planned to be tackled as part of #8915.

### Documentation

No documentation changes should be needed here.

### Additional Information

This includes changes that have been pulled from #8875.

This was originally merged in #8916 but was reverted in #8933 due to RaspberryPiFoundation/blockly-keyboard-experimentation#481. Note that this does contain a number of differences from the original PR (namely, changes in `WorkspaceSvg` and `FlyoutButton` in order to make `FlyoutButton`s focusable). Otherwise, this has the same caveats as those noted in #8938 with regards to the experimental keyboard navigation plugin.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

PR: feature Adds a feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants